#!/usr/bin/python  # 指定 Python 解释器
# -*- coding: UTF-8 -*-  # 指定源代码文件编码为 UTF-8，便于中文注释
#  语音听写（即时语音识别）流式接口：  # 模块用途说明
#  用于1分钟内的即时语音转文字技术，支持实时返回识别结果，达到一边上传音频一边获得识别文本的效果。  # 详细功能描述
import datetime  # 导入 datetime 模块（备用）
import hashlib  # 导入 hashlib 用于哈希计算
import base64  # 导入 base64 用于音频与签名编码
import hmac  # 导入 hmac 用于签名生成
import json  # 导入 json 进行数据序列化与解析
import os  # 导入 os 进行文件操作
import time  # 导入 time 控制发送间隔
from urllib.parse import urlencode  # 导入 urlencode 用于 URL 参数拼接
from wsgiref.handlers import format_date_time  # 导入 RFC1123 时间格式化函数
from datetime import datetime  # 导入 datetime.now 用于时间戳
from time import mktime  # 导入 mktime 将时间元组转换为时间戳
import websocket  # 导入 websocket 客户端库 pip install websocket-client
import ssl  # 导入 ssl 以配置证书校验
import _thread as thread  # 导入 thread 用于启动后台线程

STATUS_FIRST_FRAME = 0  # 第一帧的标识（首包，需带business参数）
STATUS_CONTINUE_FRAME = 1  # 中间帧标识（流式传输中的音频帧）
STATUS_LAST_FRAME = 2  # 最后一帧的标识（收尾包，通知服务结束）
text_file = './data.txt'  # 用于存储听写的文本（由消息回调累积写入）


class Ws_Param(object):  # 定义类用于管理 websocket 所需参数与配置
    # 初始化  # 构造函数，接收鉴权信息与音频文件路径
    def __init__(self, APPID, APIKey, APISecret, AudioFile):  # 传入 APPID、APIKey、APISecret、音频文件路径
        self.APPID = APPID  # 保存 APPID
        self.APIKey = APIKey  # 保存 APIKey
        self.APISecret = APISecret  # 保存 APISecret
        self.AudioFile = AudioFile  # 保存音频文件路径
        # 公共参数(common)  # 语音服务要求的公共字段
        self.CommonArgs = {"app_id": self.APPID}  # 设置 app_id 为 APPID
        # 业务参数(business)  # 识别业务相关配置
        self.BusinessArgs = {"domain": "iat",  # 应用领域 iat：日常用语
                             "language": "zh_cn",  # 语种 zh_cn：中文（支持简单英文识别）
                             "accent": "mandarin",  # 普通话口音
                             "vinfo": 1,  # 返回子句起止端点偏移值（1 开启）
                             "vad_eos": 10000  # 静默检测尾端（毫秒），静默 10s 认为结束
                             }

    # 生成 url，通过在请求地址后面加上鉴权相关参数  # 完成服务端认证所需的 URL 构造
    def create_url(self):  # 创建带鉴权参数的 websocket 连接地址
        # 参数1：生成 RFC1123 格式的时间戳  # 用作签名中的 date 字段
        now = datetime.now()  # 当前时间
        date = format_date_time(mktime(now.timetuple()))  # 格式化为 RFC1123 字符串

        # 参数2： 生成鉴权 authorization 参数（包括 step1-5）  # 参考官方鉴权流程
        # step1 拼接 signature 原始字段的字符串（包含 host、date、request-line）
        signature_origin = "host: " + "ws-api.xfyun.cn" + "\n"  # 主机名行
        signature_origin += "date: " + date + "\n"  # 日期行
        signature_origin += "GET " + "/v2/iat " + "HTTP/1.1"  # 请求行（方法、路径、协议）
        # step2 使用 APISecret 对 signature_origin 进行 HMAC-SHA256 加密
        signature_sha = hmac.new(self.APISecret.encode('utf-8'), signature_origin.encode('utf-8'),
                                 digestmod=hashlib.sha256).digest()  # 生成签名二进制
        # step3 将签名进行 base64 编码，得到最终 signature 字段
        signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8')  # 转为字符串
        # step4 组装 authorization 原文字符串（尚未 base64）
        authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
            self.APIKey, "hmac-sha256", "host date request-line", signature_sha)  # 填入 APIKey 与签名
        # step5 对 authorization 原文进行 base64 编码，得到最终 authorization 参数
        authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')  # 转为字符串

        # 将鉴权参数与 host 组合为字典用于 URL 查询串  # 统一收集参数
        v = {
            "authorization": authorization,  # 鉴权字段
            "date": date,  # 时间戳
            "host": "ws-api.xfyun.cn"  # 主机名
        }

        # 请求地址  # 目标 websocket 服务端地址
        url = 'wss://ws-api.xfyun.cn/v2/iat'  # 基础地址
        # 在地址后拼接鉴权参数，生成最终 url  # 将 v 转为查询串
        url = url + '?' + urlencode(v)  # 组合 URL
        # print('websocket url :', url)  # 调试输出（默认注释）
        return url  # 返回完整 URL


# 收到 websocket 消息的处理：把服务端返回的 json 数据进行解析并写入文本  # 解析增量识别结果
def on_message(ws, message):  # ws 为连接对象，message 为服务端返回的文本消息
    try:  # 异常保护，避免单次消息导致程序中断
        code = json.loads(message)["code"]  # 返回码，0 表示成功，其它表示异常
        sid = json.loads(message)["sid"]  # 本次会话 id（握手成功后第一帧请求时返回）
        if code != 0:  # 非 0 表示出错
            errMsg = json.loads(message)["message"]  # 读取错误信息
            print("sid:%s call error:%s code is:%s" % (sid, errMsg, code))  # 打印错误

        else:  # 成功返回的情况
            data = json.loads(message)["data"]["result"]["ws"]  # 取出分块结果数组
            result = ""  # 初始化结果字符串
            for i in data:  # 遍历每个分块
                for w in i["cw"]:  # 遍历候选词数组
                    result += w["w"]  # 累加识别的词到结果
            with open(text_file, 'a', encoding='utf-8') as f:  # 以追加方式写入到结果文件
                f.write(result)  # 写入本次增量文本
    except Exception as e:  # 捕获解析异常
        print("接收消息，但解析异常", e)  # 打印异常信息


# 收到 websocket 错误的处理  # 统一错误输出
def on_error(ws, error):  # websocket 报错时触发 on_error 事件
    print("错误信息:", error)  # 打印错误信息


def on_close(ws):  # websocket 关闭时触发 on_close 事件
    print("websocket连接关闭")  # 输出连接关闭提示


# 用于处理音频数据发送  # 将本地音频文件按帧发送到服务端
def data_send(ws, file, commonargs, Businessargs):  # ws 为连接，file 为音频文件路径，参数为公共与业务配置
    frameSize = 8000  # 每一帧的音频大小（字节）
    intervel = 0.04  # 发送音频间隔(单位:秒)
    status = STATUS_FIRST_FRAME  # 初始状态为第一帧

    # 业务数据流参数：根据文件类型选择编码  # 根据扩展名自动选择编码与格式
    ext = os.path.splitext(file)[1].lower()  # 提取文件扩展名并小写
    if ext in ['.wav', '.pcm']:  # 原生 PCM/WAV 情况
        data_format = "audio/L16;rate=16000"  # 原生 PCM/WAV(16k)使用 L16 格式
        data_encoding = "raw"  # 原生音频（支持单声道的 pcm）
    else:  # 其他情况
        # 默认为 mp3  # 使用 lame 编码
        data_format = "audio/L16;rate=16000"  # 仍传递 16k 采样率描述
        data_encoding = "lame"  # mp3 格式

    with open(file, "rb") as fp:  # 以二进制方式打开音频文件
        while True:  # 循环读取并发送
            buf = fp.read(frameSize)  # 读取一个 frameSize 的块
            # 文件结束  # 无数据表示到达文件末尾
            if not buf:  # 判断缓冲为空
                status = STATUS_LAST_FRAME  # 切换为最后一帧状态
            # 第一帧处理  # 首次发送需包含 common 与 business 参数
            # 发送第一帧音频，带 business 参数，appid 必须带上，只需第一帧发送  # 服务端握手与配置
            if status == STATUS_FIRST_FRAME:  # 第一帧逻辑
                d = {"common": commonargs,  # 公共参数 APPid
                     "business": Businessargs,  # 业务参数
                     "data": {"status": 0,  # 音频状态 0 : 第一帧
                              "format": data_format,  # 音频格式
                              "audio": str(base64.b64encode(buf), 'utf-8'),  # 音频内容，base64 编码
                              "encoding": data_encoding  # 编码方式
                              }
                     }
                d = json.dumps(d)  # 将请求数据转化为字符串
                ws.send(d)  # 发送数据到服务端
                if os.path.exists(text_file):  # 若结果文件已存在，先删除（避免旧内容）
                    os.remove(text_file)  # 删除旧文件
                status = STATUS_CONTINUE_FRAME  # 切换到中间帧状态

            # 中间帧处理  # 持续发送音频流
            elif status == STATUS_CONTINUE_FRAME:  # 中间帧逻辑
                d = {"data": {"status": 1,  # 音频状态 1 : 中间音频
                              "format": data_format,  # 保持格式描述
                              "audio": str(base64.b64encode(buf), 'utf-8'),  # 编码后的音频数据
                              "encoding": data_encoding  # 编码方式
                              }
                     }
                ws.send(json.dumps(d))  # 发送中间帧数据

            # 最后一帧处理  # 通知服务端音频结束
            elif status == STATUS_LAST_FRAME:  # 最后一帧逻辑
                d = {"data": {"status": 2,  # 音频状态 2 : 最后一帧
                              "format": data_format,  # 格式描述
                              "audio": str(base64.b64encode(buf), 'utf-8'),  # 最后一段数据（可能为空）
                              "encoding": data_encoding  # 编码方式
                              }
                     }
                ws.send(json.dumps(d))  # 发送最后一帧
                time.sleep(1)  # 等待服务端处理完成
                break  # 退出发送循环
            # 模拟音频采样间隔  # 控制发送频率以符合流式要求
            time.sleep(intervel)  # 睡眠指定间隔
    ws.close()  # 关闭 websocket 连接


def stt_api_get_result(APPID, APIKey, APISecret, filename):  # 语音听写入口函数，封装完整连接与发送流程
    # 实例化 websocket 参数类  # 提供鉴权与音频文件信息
    wsParam = Ws_Param(APPID=APPID,  # 传入 APPID
                       APIKey=APIKey,  # 传入 APIKey
                       APISecret=APISecret,  # 传入 APISecret
                       AudioFile=filename)  # 指定音频文件
    wsUrl = wsParam.create_url()  # 获取带鉴权的 websocket 连接地址

    def on_open(ws):  # 连接到服务器之后触发 on_open 事件
        def run(*args):  # 在独立线程中执行发送，避免阻塞主线程
            data_send(ws, wsParam.AudioFile, wsParam.CommonArgs, wsParam.BusinessArgs)  # 调用发送函数
        thread.start_new_thread(run, ())  # 启动新线程执行 run

    # 向服务器端发送 Websocket 握手并建立连接  # 配置事件回调
    # 握手成功后，客户端通过 Websocket 连接同时上传和接收数据  # 流式交互
    # 接收到服务器端的结果全部返回标识后断开 Websocket 连接  # 直至服务端结束
    websocket.enableTrace(False)  # 关闭调试输出
    ws = websocket.WebSocketApp(wsUrl,  # 创建 WebSocketApp 实例
                                on_open=on_open,  # 连接打开回调
                                on_message=on_message,  # 消息回调（接收识别结果）
                                on_error=on_error,  # 错误回调
                                on_close=on_close)  # 关闭回调
    # run_forever 是一个阻塞循环，只要 websocket 未断开就持续运行  # 维持连接
    ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})  # 禁用 SSL 证书验证，避免本地环境验证错误
